
package com.studiofortress.sf.util.collections;

import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * The Cache is designed to cache objects of B from keys A. They are stored
 * behind soft references which allows them to be garbage collected if required
 * by the JVM.
 *
 * Just to clarify, if you store a value in this cache and only this cache has a
 * reference to that value then it might be removed and garbage collected.
 *
 * The idea is to store as much as the user wishes in the Cache without any
 * worries about memory of clearing the cache themselves.
 *
 * The user should be aware that methods like containsValue and keySet might be
 * incorrect as it's possible that the values they claim are still in the cache
 * were garbage collected directly after making the method call.
 *
 * @author Joseph Lenton
 */
public class Cache<A, B> implements Map<A, B>
{
    private Map<A, SoftReference<B>> cache;

    /**
     * Trivial constructor that creates a new Cache map.
     */
    public Cache()
    {
        cache = new HashMap<A, SoftReference<B>>();
    }

    /**
     * Stores the given object in this cache. There is no guarantee that it will
     * be here permanently.
     * @param key The key for the value to be stored under.
     * @param value The value to cache, cannot be null.
     * @return The previous value stored under the given key, or null if there isn't one.
     */
    @Override
    public B put(A key, B value)
    {
        if (value == null) {
            throw new IllegalArgumentException("Null cannot be cached.");
        }

        SoftReference<B> previous = cache.put(key, new SoftReference<B>(value));
        if (previous != null) {
            return previous.get();
        } else {
            return null;
        }
    }

    /**
     * @return The number of values stored in this cache.
     */
    public int size()
    {
        clearReferences();
        return cache.size();
    }

    /**
     * @return True if this cache is empty, otherwise false.
     */
    public boolean isEmpty()
    {
        clearReferences();
        return cache.isEmpty();
    }

    public boolean containsKey(Object key)
    {
        return cache.containsKey(key);
    }

    public boolean containsValue(Object value)
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * This might return null for values that were stored in the cache because
     * they have been garbage collected.
     * @param key The key for the value to retrieve.
     * @return Null if the key is not found, otherwise the value stored under the stated key.
     */
    public B get(Object key)
    {
        return cache.get(key).get();
    }

    public B remove(Object key)
    {
        return cache.remove(key).get();
    }

    public void putAll(Map<? extends A, ? extends B> addingMap)
    {
        for (Entry<? extends A, ? extends B> eSet : addingMap.entrySet()) {
            put(eSet.getKey(), eSet.getValue());
        }
    }

    /**
     * Cleares the cache removing all the items stored in it.
     */
    public void clear()
    {
        cache.clear();
    }

    /**
     * Values stored in the cache may be garbage collected at any time. This
     * goes through and removes all the keys to values which have been garbage
     * collected.
     */
    public void clearReferences()
    {
        for (Entry<A, SoftReference<B>> eSet : cache.entrySet()) {
            if (eSet.getValue().get() == null) {
                cache.remove(eSet.getKey());
            }
        }
    }

    /**
     * @return A set contianing all the keys in this cache where their values still exist.
     */
    public Set<A> keySet()
    {
        clearReferences();
        return cache.keySet();
    }

    /**
     * @return A collection containing all the values stored in this cache.
     */
    public Collection<B> values()
    {
        Collection values = new LinkedList<B>();

        for (SoftReference<B> ref : cache.values()) {
            B val = ref.get();
            if (val != null) {
                values.add(val);
            }
        }

        return values;
    }

    public Set<Entry<A, B>> entrySet()
    {
        Set<Entry<A, B>> entrySet = new HashSet<Entry<A, B>>();

        for (A a : keySet()) {
            entrySet.add(new CacheEntry(a));
        }
        
        return entrySet;
    }

    private class CacheEntry implements Entry<A, B>
    {
        private final A key;

        public CacheEntry(A key)
        {
            this.key = key;
        }

        public A getKey()
        {
            return key;
        }

        public B getValue()
        {
            return Cache.this.get(key);
        }

        public B setValue(B value)
        {
            return Cache.this.put(key, value);
        }
    }
}
